﻿#region Copyright(c) 2006 ZO, All right reserved.
// -----------------------------------------------------------------------------
// Copyright(c) 2006 ZO, All right reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   1.  Redistribution of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//   2.  Redistribution in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//   3.  The name of the author may not be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// -----------------------------------------------------------------------------
#endregion


#region Using directives

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using System.Threading;
using ZO.SmartCore.Core;
using ZO.SmartCore.Helpers;
using ZO.SmartCore.Net.Sockets;
using ZO.SmartCore.Protocols.XMPP.Protocol.Common;
using ZO.SmartCore.Protocols.XMPP.SASL.Core;
using ZO.SmartCore.Protocols.XMPP.SASL.Mechanisms;
using ZO.SmartCore.Protocols.XMPP.SASL.TLS;
using ZO.SmartCore.Xml;
using Stream = ZO.SmartCore.Protocols.XMPP.Protocol.Client.Stream;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Net.Sockets;
using System.Security.Authentication;
using ZO.SmartCore.My4CSharp;
using IOStream = System.IO.Stream;
using ZO.SmartCore.Protocols.XMPP.Properties;
#endregion

namespace ZO.SmartCore.Protocols.XMPP.Core
{

    /// <summary>
    /// client to use to access the Jabber server
    /// </summary>
    public class XmppClient : TCPClient
    {

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="XmppClient"/> class.
        /// </summary>
        public XmppClient()
            : base(Encoding.UTF8) { }

        #endregion

        #region Destructor

        #endregion

        #region Fields
        internal const int DEFAULT_PORT = 5222;
        internal const string DEFAULT_SERVER = "jabber.org";
        private string _StreamVersion = "1.0";
        private XmppConnectionState _State = XmppConnectionState.Disconnected;
        private string _Username;

        private string _Password;
        private XmlLang _Language = XmlLang.English;

        private Stream xmppStream;

        private MemoryStream ms;

        private ManualResetEvent recieveDone = new ManualResetEvent(false);
        private ManualResetEvent starttlsDone = new ManualResetEvent(false);

        private IOStream clientStream;

        internal static SocketStatus DataStatus = SocketStatus.None;
        #endregion



        #region Events

        #endregion

        #region Operators

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the stream version.
        /// </summary>
        /// <value>The stream version.</value>
        public string StreamVersion
        {
            get
            {
                return this._StreamVersion;
            }
            set
            {
                this._StreamVersion = value;
            }
        }


        /// <summary>
        /// Gets the connection state.
        /// </summary>
        /// <value>The xmpp state.</value>
        public new XmppConnectionState State
        {
            get
            {
                return this._State;
            }
            private set
            {
                this._State = value;
            } // set
        }


        /// <summary>
        /// Gets or sets the username.
        /// </summary>
        /// <value>The username.</value>
        public string Username
        {
            get
            {
                return this._Username;
            }
            set
            {
                this._Username = value;
            }
        }

        /// <summary>
        /// Gets or sets the password.
        /// </summary>
        /// <value>The password.</value>
        public string Password
        {
            get
            {
                return this._Password;
            }
            set
            {
                this._Password = value;
            }
        }

        /// <summary>
        /// Gets or sets the language used by the client.
        /// </summary>
        /// <remarks>currently only english</remarks>
        public XmlLang Language
        {
            get
            {
                return _Language;
            }
            set
            {
                this._Language = value;
            }
        }


        #endregion

        #region Methods

        /// <summary>
        /// Send data to the server, specifying a buffer, offset, count and SocketFlags value.
        /// </summary>
        /// <param name="buffer">Source memory location for the data to send.</param>
        /// <param name="offset">Starting offset within buffer for sending.</param>
        /// <param name="count">Number of bytes to send.</param>
        /// <param name="socketFlags">A bitwise combination of special use sending parameters.</param>
        /// <returns></returns>
        public override void Send(byte[] buffer, int offset, int count, SocketFlags socketFlags)
        {

            if (clientStream == null)
            {
                base.Send(buffer, offset, count, socketFlags);
            }
            else
            {
                clientStream.Write(buffer, offset, count);
            }

        }

        /// <summary>
        /// Raises the <see cref="BaseClientSocket.Connecting"/> event.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnConnecting(EventArgs e)
        {
            this.State = XmppConnectionState.Connecting;
            base.OnConnecting(e);
        }

        /// <summary>
        /// Sends the specified Xml element.
        /// </summary>
        /// <param name="element">The element to send.</param>
        public void Send(Element element)
        {

            this.Send(element.ToString());

        }


        /// <summary>
        /// Raises the <see cref="BaseClientSocket.ConnectCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        protected override void OnConnectCompleted(EventArgs e)
        {
            this.State = XmppConnectionState.Connected;

            base.OnConnectCompleted(e);

            string text = this.OpenOrCloseStream(true);

            DataStatus = SocketStatus.Sent;

            this.Send(text);
            That.Application.Log.Write(text);


            new SimpleCaller(delegate
            {
                while (this.Connected)
                {

                    this.ReceiveAsync();

                    Thread.Sleep(100);
                }
            }
            ).BeginInvoke(null, null);

        }



        /// <summary>
        /// Raises the <see cref="BaseClientSocket.Disconnecting"/> event.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnDisconnecting(EventArgs e)
        {
            this.State = XmppConnectionState.Disconnecting;
            string text = this.OpenOrCloseStream(false);

            DataStatus = SocketStatus.Sent;

            this.Send(text);

            That.Application.Log.Write(text);

            // Yield the rest of this time slice.
            Thread.Sleep(0);
            base.OnDisconnecting(e);
        }



        /// <summary>
        /// Raises the <see cref="BaseClientSocket.DisconnectCompleted"/> event.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnDisconnectCompleted(EventArgs e)
        {
            this.State = XmppConnectionState.Disconnected;
            base.OnDisconnectCompleted(e);
        }

        private string OpenOrCloseStream(bool open)
        {
            StringBuilder builder1 = new StringBuilder();

            if (open)
            {

                builder1.Append("<?xml version='1.0' encoding='utf-8' ?>");
                builder1.Append("<stream:stream");
                builder1.Append(" to='" + this.Address.Domain + "'");
                builder1.Append(" xmlns='jabber:client'");
                builder1.Append(" xmlns:stream='http://etherx.jabber.org/streams'");

                if (!String.IsNullOrEmpty(this.StreamVersion))
                {
                    builder1.Append(" version='" + this.StreamVersion + "'");
                }

                if (this.Language != XmlLang.None)
                {
                    builder1.Append(" xml:lang='" + ReflectionHelper.GetDescription(this.Language) + "'");
                }

                builder1.Append(">");
            }
            else
            {
                builder1.Append("</stream:stream>");
            }
            return builder1.ToString();

        }

        /// <summary>
        /// Raises the <see cref="TCPClient.SendCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="T:SmartCore.Net.Sockets.SocketEventArgs"/> instance containing the event data.</param>
        protected override void OnSendCompleted(SocketEventArgs e)
        {
            base.OnSendCompleted(e);

            if (e.Error == null & e.Segment.Count > 0)
            {
                That.Application.Log.Write(e.Segment.ToString());

            }

        }

        /// <summary>
        /// Raises the <see cref="TCPClient.RecieveCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="T:SmartCore.Net.Sockets.SocketEventArgs"/> instance containing the event data.</param>
        protected override void OnRecieveCompleted(SocketEventArgs e)
        {
            DataStatus = SocketStatus.Recieve;

            lock (this)
            {
                byte[] buffer = new byte[1024];

                if (ms != null)
                {
                    ms.Close();
                }

                ms = new MemoryStream();

                if (e.Segment.Count > 0)
                {
                    ms.Write(e.Segment.Buffer, e.Segment.Offset, e.Segment.Count);

                    That.Application.Log.Write(e.Segment.ToString());

                }

                int num;
                while (this.Available > 0)
                {
                    num = this.Receive(buffer);

                    if (num > 0)
                    {
                        ms.Write(buffer, 0, num);

                        That.Application.Log.Write(Encoding.UTF8.GetString(buffer, 0, num));

                    }
                    Thread.Sleep(0);

                }

            }

            DataStatus = SocketStatus.None;


            if (ms.Length != 0)
            {
                string text;
                lock (this)
                {
                    text = Encoding.UTF8.GetString(ms.ToArray());

                }

                e.Segment.Buffer = e.Segment.Encoding.GetBytes(text);
                Thread.Sleep(0);

                if (this.State == XmppConnectionState.Connected || this.State == XmppConnectionState.Secured)
                {


                    if (text.Contains("<stream:stream"))
                    {
                        if (!text.EndsWith("</stream:stream>"))
                        {
                            text = text + "</stream:stream>";
                        }
                        else
                        {
                            this.Disconnect();
                        }
                    }

                    if (xmppStream == null)
                    {
                        xmppStream = new Stream();
                        xmppStream.LoadXml(text);

                        //check for stream error
                        if (xmppStream.Error != null)
                        {
                            base.OnError(new ExceptionEventArgs(new XmppException(xmppStream.Error.Message)));
                            this.Disconnect();
                            return;
                        }

                        //check for stream feature
                        //Server sends the STARTTLS extension to client along with authentication mechanisms and any other stream features:
                        if (xmppStream.HasChildElement("features"))
                        {
                            StreamFeature feature = new StreamFeature();

                            feature.Load(xmppStream.GetChildElement("features").Clone());

                            HandleStreamFeatures(feature);
                        }

                    }

                }

                if (this.State == XmppConnectionState.Securing)
                {
                    That.Application.Log.Write(text);
                }

                Thread.Sleep(0);


                base.OnRecieveCompleted(e);

            }

        }


        /// <summary>
        /// Handles the mechanism.
        /// </summary>
        /// <param name="feature">The feature.</param>
        private void HandleStreamFeatures(StreamFeature feature)
        {
            if (this.State == XmppConnectionState.Connected)
            {
                if (feature.SupportsStartTls)
                {

                    this.State = XmppConnectionState.Securing;
                    DataStatus = SocketStatus.Sent;

                    //Client sends the STARTTLS command to server:
                    string text = new StartTLS().ToString();
                    this.Send(text);
                    Thread.Sleep(0);
                    That.Application.Log.Write(text);

                    byte[] buffer = new byte[1024];

                    int num = 0;

                    int count = 0;

                    DataStatus = SocketStatus.Recieve;

                    while (num <= 0)
                    {
                        num = this.Receive(buffer);

                        ++count;

                        if (count > 3)
                        {
                            break;
                        }
                    }

                    if (num <= 0)
                    {
                        this.Disconnect();
                        return;
                    }

                    text = System.Text.Encoding.UTF8.GetString(buffer, 0, num);

                    That.Application.Log.Write(text);
                    //Server informs client that it is allowed to proceed:
                    if (text.Contains("proceed"))
                    {

                        xmppStream = null;

                        //Authenticate TLS
                        SslStream sslStream = new SslStream(new NetworkStream(this.Socket, false), true,
                            new RemoteCertificateValidationCallback(ValidateServerCertificate),
                            new LocalCertificateSelectionCallback(SelectLocalCertificate));
                        this.clientStream = sslStream;
                        try
                        {
                            sslStream.AuthenticateAsClient(this.Address.Domain, null, SslProtocols.Tls, false);

                            //Open new stream
                            text = OpenOrCloseStream(true);
                            this.Send(text);

                        }
                        catch (AuthenticationException e)
                        {
                            DataStatus = SocketStatus.Error;

                            That.Application.Log.Write(e.ToString());
                            throw;
                        }


                        //while (this.Available > 0)
                        //{
                        //    num = this.Receive(buffer);

                        //    text = System.Text.Encoding.UTF8.GetString(buffer, 0, num);

                        //    //Server informs client that TLS negotiation has failed and closes both stream and TCP connection:
                        //    if (text.Contains("failure"))
                        //    {
                        //        this.Disconnect();
                        //        return;
                        //    }

                        //    That.Application.Log.Write(text);
                        //    Thread.Sleep(0);

                        //}

                        this.State = XmppConnectionState.Secured;
                    }

                    //Server informs client that TLS negotiation has failed and closes both stream and TCP connection:
                    if (text.Contains("failure"))
                    {
                        this.Disconnect();
                        return;
                    }


                    return;
                }




                if (feature.SupportsMechanisms)
                {
                    ReadOnlyCollection<Mechanism> mechanisms = feature.Mechanisms.AllMechanisms;

                    foreach (Mechanism mechanism in mechanisms)
                    {
                        if (mechanism.Type == MechanismType.XGoogleToken)
                        {
                            break;
                        }


                    }
                }
            }
        }



        private static X509Certificate SelectLocalCertificate(
            object sender,
            string targetHost,
            X509CertificateCollection localCertificates,
            X509Certificate remoteCertificate,
            string[] acceptableIssuers)
        {
            Console.WriteLine("Client is selecting a local certificate.");
            if (acceptableIssuers != null &&
                acceptableIssuers.Length > 0 &&
                localCertificates != null &&
                localCertificates.Count > 0)
            {
                // Use the first certificate that is from an acceptable issuer.
                foreach (X509Certificate certificate in localCertificates)
                {
                    string issuer = certificate.Issuer;
                    if (Array.IndexOf(acceptableIssuers, issuer) != -1)
                        return certificate;
                }
            }
            if (localCertificates != null &&
                localCertificates.Count > 0)
                return localCertificates[0];

            return null;
        }



        /// <summary>
        /// Validates the server certificate.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="certificate">The certificate.</param>
        /// <param name="chain">The chain.</param>
        /// <param name="sslPolicyErrors">The SSL policy errors.</param>
        /// <returns></returns>
        private static bool ValidateServerCertificate(
              object sender,
              X509Certificate certificate,
              X509Chain chain,
              SslPolicyErrors sslPolicyErrors)
        {
            // allow this client to communicate with unauthenticated servers.
            return true;
        }



        #endregion
    }
}
